package br.edu.unifei.mestrado.mn;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.edu.unifei.mestrado.commons.graph.GraphWrapper;
import br.edu.unifei.mestrado.commons.mn.GN;
import br.edu.unifei.mestrado.commons.partition.BestPartition;
import br.edu.unifei.mestrado.commons.partition.NullPartition;
import br.edu.unifei.mestrado.commons.view.ViewListener;

public abstract class TwoWayMultinivel implements ViewListener {

	private Logger logger = LoggerFactory.getLogger(this.getClass());

	private static final int DONT_KNOW_CUT_WEIGHT = -1;

	public static final int NUMBER_OF_NODES_TO_COARSE = 9;

	private CoarseHelper coarseHelper;

	public TwoWayMultinivel() {
		coarseHelper = new CoarseHelper();
	}

	// /**
	// * Retorna uma aresta wrapper para a aresta verdadeira entre va e vb. Retornar null se não exisistir a
	// * aresta verdadeira.
	// *
	// * @param va
	// * @param vb
	// * @return
	// */
	// protected abstract RelationshipWrapper getArestaLinking(NodeWrapper va, NodeWrapper vb);

	public abstract void readGraph();

	protected abstract GraphWrapper getGrafo();

	/**
	 * Cria um novo grafo, dependendo se está em memória ou banco.
	 * 
	 * @param level
	 * @return
	 */
	protected abstract GraphWrapper createNewGraph(Integer level);

	/**
	 * Faz o refinamento do grafo usando o algoritmo definido na classe filha.
	 * 
	 * @param graph
	 * @param level
	 * @return
	 */
	protected abstract BestPartition refineGraph(GraphWrapper graph, int level);

	/**
	 * 1) Executa o particionamento 2-way usando o algoritmo definido na classe filha.
	 */
	protected abstract BestPartition executePartition(GraphWrapper graph);

	protected void preprocess(GraphWrapper initialGraph) {
		// stub method that can be used by sub class.
	}

	/**
	 * Executa a expanção do grafo. <br>
	 * <br>
	 * 1) Repassa o particionamento de volta para o grafo mais fino. <br>
	 * 2) Melhora o particionamento usando o refinamento.
	 */
	protected BestPartition uncoarseGraph(GN gn, int level, BestPartition bestPartM_1) {

		GraphWrapper grafoM_1 = gn.getGraph(level);
		GraphWrapper grafoM = gn.getGraph(level + 1);

		gn.projectPartitionBack(grafoM_1, grafoM);

		// // so para mostrar na tela
		// updateView(grafoM, level, "CN: " + level, new BestPartition(level));
		// updateViewCoarsed(grafoM_1, level - 1, "CN: " + (level - 1), bestPartM_1.getCutWeight());

		// faz o melhoramento do particionamento projetado

		BestPartition part = null;
		grafoM_1.beginTransaction();
		try {
			part = refineGraph(grafoM_1, level);
			grafoM_1.endTransaction();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			grafoM_1.finish();
		}

		logger.info("Partition of level " + level);
		part.printSets();
		return part;
	}

	/**
	 * Execução geral do multinivel <br>
	 * 1) Coarse (contrai) <br>
	 * 2) Partitioning <br>
	 * 3) Uncoarse (expande)
	 * 
	 */
	protected void executeMultiNivel() {
		GraphWrapper initialGraph = getGrafo();
		if (initialGraph.getSizeNodes() > 200) {
			setShowView(false);
			logger.warn("Desabilitando exibição dos do grafo. qtd de vertices > 200");
		}
		initView(initialGraph);

		preprocess(initialGraph);

		int level = 0;
		GN gn = new GN();
		gn.setGraph(initialGraph, new NullPartition(level), level);

		// fase de coarsening
		level = executeCoarsening(level, gn);
		// fase de particionamento
		BestPartition part = executePartition(gn.getGraph(level));
		logger.warn("Corte após particionamento: " + part.getCutWeight());

		// so para mostrar na tela
		updateView(gn.getGraph(level), level, "PN: " + level, part.getCutWeight());

		// fase de uncoarsening
		level--;
		logger.warn("Expandindo o grafo...");
		while (level >= 0) {
			part = uncoarseOneLevel(level, gn, part);
			level--;
		}
		logger.warn("Fim da fase de uncoarsening...");
	}

	private int executeCoarsening(int level, GN gn) {
		logger.warn("Contraindo o grafo...");
		logger.warn("Qtd de vertices do nivel: " + level + " :" + gn.getGraph(level).getSizeNodes());

		// UTIL: usado quando o grafo é desconexo. Quando chega em um ponto onde fica somente 1 node em cada
		// conjunto, daí não contrai mais.
		boolean gotLessNodes = true;
		while (gotLessNodes && gn.getGraph(level).getSizeNodes() > NUMBER_OF_NODES_TO_COARSE) {
			GraphWrapper newGraph = createNewGraph(level + 1);
			gotLessNodes = coarseHelper.coarseOneLevel(level + 1, gn, newGraph);
			if (gotLessNodes) { // UTIL: se diminuiu o número de vertices, então continua
				level++;

				// so para mostrar na tela
				updateView(gn.getGraph(level), level, "CN: " + level, DONT_KNOW_CUT_WEIGHT);
				updateViewCoarsed(gn.getGraph(level - 1), level - 1, "CN: " + (level - 1),
						DONT_KNOW_CUT_WEIGHT);
			}
		}
		return level;
	}

	private BestPartition uncoarseOneLevel(int level, GN gn, BestPartition bestPart) {
		logger.warn("Iniciando uncoarse do NIVEL: " + level);
		BestPartition part = uncoarseGraph(gn, level, bestPart);
		logger.warn("Qtd de vertices do nivel: " + level + " :" + gn.getGraph(level).getSizeNodes()
				+ " Corte: " + part.getCutWeight());

		// so para mostrar na tela
		if (level > 0) { // para não dar erro no ultimo nivel
			updateView(gn.getGraph(level - 1), level - 1, "CN: " + (level - 1), bestPart.getCutWeight());
		}
		updateViewCoarsed(gn.getGraph(level), level, "CN: " + (level), part.getCutWeight());
		return part;
	}

	public void execute() {
		try {
			getGrafo().beginTransaction();
			readGraph();
			getGrafo().endTransaction();
		} finally {
			getGrafo().finish();
		}

		try {
			long time = System.currentTimeMillis();
			executeMultiNivel();
			time = System.currentTimeMillis() - time;
			logger.warn("Fim do Multinivel. Tempo gasto: " + time + " ms File: "
					+ getGrafo().getGraphFileName() + " - Hora Termino: " + new Date());
		} catch (Throwable e) {
			logger.error("Erro executando Multinivel.", e);
		} finally {
			getGrafo().finish();
		}
	}
}
